home *** CD-ROM | disk | FTP | other *** search
/ Personal Computer World 2008 February / PCWFEB08.iso / Software / Freeware / Miro 1.0 / Miro_Installer.exe / Miro_Downloader.exe / item.pyc (.txt) < prev    next >
Encoding:
Python Compiled Bytecode  |  2007-11-12  |  55.9 KB  |  2,082 lines

  1. # Source Generated with Decompyle++
  2. # File: in.pyc (Python 2.5)
  3.  
  4. from copy import copy
  5. from datetime import datetime, timedelta
  6. from gtcache import gettext as _
  7. from math import ceil
  8. from xhtmltools import unescape, xhtmlify
  9. from xml.sax.saxutils import unescape
  10. from util import checkU, returnsUnicode, checkF, returnsFilename, quoteUnicodeURL, stringify
  11. from platformutils import FilenameType
  12. import locale
  13. import os
  14. import os.path as os
  15. import urllib
  16. import shutil
  17. import traceback
  18. from download_utils import cleanFilename, nextFreeFilename
  19. from feedparser import FeedParserDict
  20. from database import DDBObject, defaultDatabase, ObjectNotFoundError
  21. from database import DatabaseConstraintError
  22. from databasehelper import makeSimpleGetSet
  23. from iconcache import IconCache
  24. from templatehelper import escape, quoteattr
  25. import types
  26. import app
  27. import template
  28. import downloader
  29. import config
  30. import dialogs
  31. import eventloop
  32. import feed
  33. import filters
  34. import menu
  35. import prefs
  36. import resources
  37. import views
  38. import random
  39. import indexes
  40. import util
  41. import adscraper
  42. import autodler
  43. import moviedata
  44. import logging
  45. import platformutils
  46. import filetypes
  47. import searchengines
  48. import fileutil
  49. import imageresize
  50. _charset = locale.getpreferredencoding()
  51.  
  52. class Item(DDBObject):
  53.     '''An item corresponds to a single entry in a feed. It has a single url
  54.     associated with it.
  55.     '''
  56.     SMALL_ICON_SIZE = (108, 81)
  57.     BIG_ICON_SIZE = (226, 170)
  58.     ICON_CACHE_SIZES = [
  59.         SMALL_ICON_SIZE,
  60.         BIG_ICON_SIZE]
  61.     
  62.     def __init__(self, entry, linkNumber = 0, feed_id = None, parent_id = None):
  63.         self.feed_id = feed_id
  64.         self.parent_id = parent_id
  65.         self.isContainerItem = None
  66.         self.isVideo = False
  67.         self.seen = False
  68.         self.autoDownloaded = False
  69.         self.pendingManualDL = False
  70.         self.downloadedTime = None
  71.         self.watchedTime = None
  72.         self.pendingReason = u''
  73.         self.entry = entry
  74.         self.expired = False
  75.         self.keep = False
  76.         self.videoFilename = FilenameType('')
  77.         self.eligibleForAutoDownload = True
  78.         self.duration = None
  79.         self.screenshot = None
  80.         self.resized_screenshots = { }
  81.         self.resumeTime = 0
  82.         self.iconCache = IconCache(self)
  83.         self.linkNumber = linkNumber
  84.         self.creationTime = datetime.now()
  85.         self.updateReleaseDate()
  86.         self._initRestore()
  87.         self._lookForFinishedDownloader()
  88.         DDBObject.__init__(self)
  89.         self.splitItem()
  90.  
  91.     
  92.     def onRestore(self):
  93.         if self.iconCache == None:
  94.             self.iconCache = IconCache(self)
  95.         else:
  96.             self.iconCache.dbItem = self
  97.             self.iconCache.requestUpdate()
  98.         if not hasattr(self, 'isContainerItem'):
  99.             self.isContainerItem = None
  100.         
  101.         self._initRestore()
  102.  
  103.     
  104.     def _initRestore(self):
  105.         '''Common code shared between onRestore and __init__.'''
  106.         self.selected = False
  107.         self.active = False
  108.         self.childrenSeen = None
  109.         self.downloader = None
  110.         self.expiring = None
  111.         self.showMoreInfo = False
  112.         self.updating_movie_info = False
  113.  
  114.     
  115.     def _lookForFinishedDownloader(self):
  116.         dler = downloader.lookupDownloader(self.getURL())
  117.         if dler and dler.isFinished():
  118.             self.downloader = dler
  119.             dler.addItem(self)
  120.         
  121.  
  122.     (getSelected, setSelected) = makeSimpleGetSet(u'selected', changeNeedsSave = False)
  123.     (getActive, setActive) = makeSimpleGetSet(u'active', changeNeedsSave = False)
  124.     
  125.     def getSelectedState(self, view):
  126.         currentView = app.controller.selection.itemListSelection.currentView
  127.         if not (self.selected) or view != currentView:
  128.             return u'normal'
  129.         elif not self.active:
  130.             return u'selected-inactive'
  131.         else:
  132.             return u'selected'
  133.  
  134.     getSelectedState = returnsUnicode(getSelectedState)
  135.     
  136.     def toggleShowMoreInfo(self):
  137.         self.showMoreInfo = not (self.showMoreInfo)
  138.         self.signalChange(needsSave = False, needsUpdateXML = True)
  139.  
  140.     
  141.     def getMoreInfoState(self):
  142.         if self.showMoreInfo:
  143.             return u'more-info'
  144.         
  145.         return u''
  146.  
  147.     getMoreInfoState = returnsUnicode(getMoreInfoState)
  148.     
  149.     def findChildVideos(self):
  150.         '''If this item points to a directory, return the set all video files
  151.         under that directory.
  152.         '''
  153.         videos = set()
  154.         filename_root = self.getFilename()
  155.         if os.path.isdir(filename_root):
  156.             for dirpath, dirnames, filenames in os.walk(filename_root):
  157.                 for name in filenames:
  158.                     filename = os.path.join(dirpath, name)
  159.                     if filetypes.isVideoFilename(filename) or filetypes.isAudioFilename(filename):
  160.                         videos.add(filename)
  161.                         continue
  162.                 
  163.             
  164.         
  165.         return videos
  166.  
  167.     
  168.     def findNewChildren(self):
  169.         '''If this feed is a container item, walk through its directory and
  170.         find any new children.  Returns True if it found childern and ran
  171.         signalChange().
  172.         '''
  173.         filename_root = self.getFilename()
  174.         if not self.isContainerItem:
  175.             return False
  176.         
  177.         if self.getState() == 'downloading':
  178.             return False
  179.         
  180.         videos = self.findChildVideos()
  181.         for child in self.getChildren():
  182.             videos.discard(child.getFilename())
  183.         
  184.         for video in videos:
  185.             if not video.startswith(filename_root):
  186.                 raise AssertionError
  187.             offsetPath = video[len(filename_root):]
  188.             if offsetPath[0] == '/':
  189.                 offsetPath = offsetPath[1:]
  190.             
  191.             FileItem(video, parent_id = self.id, offsetPath = offsetPath)
  192.         
  193.         if videos:
  194.             self.signalChange()
  195.             return True
  196.         
  197.         return False
  198.  
  199.     
  200.     def splitItem(self):
  201.         '''returns True if it ran signalChange()'''
  202.         if self.isContainerItem is not None:
  203.             return self.findNewChildren()
  204.         
  205.         if not isinstance(self, FileItem):
  206.             if self.downloader is None or not self.downloader.isFinished():
  207.                 return False
  208.             
  209.         filename_root = self.getFilename()
  210.         if os.path.isdir(filename_root):
  211.             videos = self.findChildVideos()
  212.             if len(videos) > 1:
  213.                 self.isContainerItem = True
  214.                 for video in videos:
  215.                     if not video.startswith(filename_root):
  216.                         raise AssertionError
  217.                     offsetPath = video[len(filename_root):]
  218.                     if offsetPath[0] == '/':
  219.                         offsetPath = offsetPath[1:]
  220.                     
  221.                     FileItem(video, parent_id = self.id, offsetPath = offsetPath)
  222.                 
  223.             elif len(videos) == 1:
  224.                 self.isContainerItem = False
  225.                 for video in videos:
  226.                     if not video.startswith(filename_root):
  227.                         raise AssertionError
  228.                     self.videoFilename = video[len(filename_root):]
  229.                     if self.videoFilename[0] in ('/', '\\'):
  230.                         self.videoFilename = self.videoFilename[1:]
  231.                     
  232.                     self.isVideo = True
  233.                 
  234.             elif not self.getFeedURL().startswith('dtv:directoryfeed'):
  235.                 target_dir = config.get(prefs.NON_VIDEO_DIRECTORY)
  236.                 if not filename_root.startswith(target_dir):
  237.                     if isinstance(self, FileItem):
  238.                         self.migrate(target_dir)
  239.                     else:
  240.                         self.downloader.migrate(target_dir)
  241.                 
  242.             
  243.             self.isContainerItem = False
  244.         else:
  245.             self.isContainerItem = False
  246.             self.videoFilename = FilenameType('')
  247.             self.isVideo = True
  248.         self.signalChange()
  249.         return True
  250.  
  251.     
  252.     def removeFromPlaylists(self):
  253.         itemIDIndex = indexes.playlistsByItemID
  254.         view = views.playlists.filterWithIndex(itemIDIndex, self.getID())
  255.         for playlist in view:
  256.             playlist.removeItem(self)
  257.         
  258.         view = views.playlistFolders.filterWithIndex(itemIDIndex, self.getID())
  259.         for playlist in view:
  260.             playlist.removeItem(self)
  261.         
  262.  
  263.     
  264.     def updateReleaseDate(self):
  265.         
  266.         try:
  267.             self.releaseDateObj = datetime(*self.getFirstVideoEnclosure().updated_parsed[0:7])
  268.         except:
  269.             
  270.             try:
  271.                 self.releaseDateObj = datetime(*self.entry.updated_parsed[0:7])
  272.             self.releaseDateObj = datetime.min
  273.  
  274.  
  275.  
  276.     
  277.     def checkConstraints(self):
  278.         if self.feed_id is not None:
  279.             
  280.             try:
  281.                 obj = self.dd.getObjectByID(self.feed_id)
  282.             except ObjectNotFoundError:
  283.                 raise DatabaseConstraintError('my feed (%s) is not in database' % self.feed_id)
  284.  
  285.             if not isinstance(obj, feed.Feed):
  286.                 msg = 'feed_id points to a %s instance' % obj.__class__
  287.                 raise DatabaseConstraintError(msg)
  288.             
  289.         
  290.         if self.parent_id is not None:
  291.             
  292.             try:
  293.                 obj = self.dd.getObjectByID(self.parent_id)
  294.             except ObjectNotFoundError:
  295.                 raise DatabaseConstraintError('my parent (%s) is not in database' % self.parent_id)
  296.  
  297.             if not isinstance(obj, Item):
  298.                 msg = 'parent_id points to a %s instance' % obj.__class__
  299.                 raise DatabaseConstraintError(msg)
  300.             
  301.             if obj.isContainerItem is not None and not (obj.isContainerItem):
  302.                 msg = 'parent_id is not a containerItem'
  303.                 raise DatabaseConstraintError(msg)
  304.             
  305.         
  306.         if self.parent_id is None and self.feed_id is None:
  307.             raise DatabaseConstraintError('feed_id and parent_id both None')
  308.         
  309.         if self.parent_id is not None and self.feed_id is not None:
  310.             raise DatabaseConstraintError('feed_id and parent_id both not None')
  311.         
  312.  
  313.     
  314.     def signalChange(self, needsSave = True, needsUpdateXML = True):
  315.         self.expiring = None
  316.         
  317.         try:
  318.             del self._state
  319.         except:
  320.             pass
  321.  
  322.         
  323.         try:
  324.             del self._size
  325.         except:
  326.             pass
  327.  
  328.         if needsUpdateXML:
  329.             
  330.             try:
  331.                 del self._itemXML
  332.  
  333.         
  334.         DDBObject.signalChange(self, needsSave = needsSave)
  335.  
  336.     
  337.     def getItemXML(self, viewName):
  338.         
  339.         try:
  340.             xml = self._itemXML
  341.         except AttributeError:
  342.             self._calcItemXML()
  343.             xml = self._itemXML
  344.  
  345.         return xml.replace(self._XMLViewName, viewName)
  346.  
  347.     
  348.     def _calcItemXML(self):
  349.         self._XMLViewName = 'view%dview' % random.randint(9999999, 99999999)
  350.         self._itemXML = template.fillStaticTemplate('download-item-inner', onlyBody = True, this = self, viewName = self._XMLViewName, templateState = 'unknown')
  351.         checkU(self._itemXML)
  352.  
  353.     
  354.     def getViewed(self):
  355.         
  356.         try:
  357.             return self._feed.lastViewed >= self.creationTime
  358.         except:
  359.             return self.creationTime <= self.getFeed().lastViewed
  360.  
  361.  
  362.     
  363.     def getFirstVideoEnclosure(self):
  364.         
  365.         try:
  366.             return self._firstVidEnc
  367.         except:
  368.             self._calcFirstEnc()
  369.             return self._firstVidEnc
  370.  
  371.  
  372.     
  373.     def _calcFirstEnc(self):
  374.         self._firstVidEnc = getFirstVideoEnclosure(self.entry)
  375.  
  376.     
  377.     def getFirstVideoEnclosureType(self):
  378.         enclosure = self.getFirstVideoEnclosure()
  379.         if enclosure and enclosure.has_key('type'):
  380.             return enclosure['type']
  381.         
  382.  
  383.     getFirstVideoEnclosureType = returnsUnicode(getFirstVideoEnclosureType)
  384.     
  385.     def getURL(self):
  386.         self.confirmDBThread()
  387.         videoEnclosure = self.getFirstVideoEnclosure()
  388.         if videoEnclosure is not None and 'url' in videoEnclosure:
  389.             return quoteUnicodeURL(videoEnclosure['url'].replace('+', '%20'))
  390.         else:
  391.             return u''
  392.  
  393.     getURL = returnsUnicode(getURL)
  394.     
  395.     def getQuotedURL(self):
  396.         return urllib.quote_plus(urllib.unquote(self.getURL().encode('ascii'))).decode('ascii')
  397.  
  398.     getQuotedURL = returnsUnicode(getQuotedURL)
  399.     
  400.     def hasSharableURL(self):
  401.         '''Does this item have a URL that the user can share with others?
  402.  
  403.         This returns True when the item has a non-file URL.
  404.         '''
  405.         url = self.getURL()
  406.         if url != u'':
  407.             pass
  408.         return not url.startswith(u'file:')
  409.  
  410.     
  411.     def getFeed(self):
  412.         
  413.         try:
  414.             return self._feed
  415.         except:
  416.             if self.feed_id is not None:
  417.                 self._feed = self.dd.getObjectByID(self.feed_id)
  418.             elif self.parent_id is not None:
  419.                 self._feed = self.getParent().getFeed()
  420.             else:
  421.                 self._feed = None
  422.             return self._feed
  423.  
  424.  
  425.     
  426.     def getParent(self):
  427.         
  428.         try:
  429.             return self._parent
  430.         except:
  431.             if self.parent_id is not None:
  432.                 self._parent = self.dd.getObjectByID(self.parent_id)
  433.             else:
  434.                 self._parent = self
  435.             return self._parent
  436.  
  437.  
  438.     
  439.     def getFeedURL(self):
  440.         return self.getFeed().getURL()
  441.  
  442.     getFeedURL = returnsUnicode(getFeedURL)
  443.     
  444.     def feedExists(self):
  445.         if self.feed_id:
  446.             pass
  447.         return self.dd.idExists(self.feed_id)
  448.  
  449.     
  450.     def getChildren(self):
  451.         if self.isContainerItem:
  452.             return views.items.filterWithIndex(indexes.itemsByParent, self.id)
  453.         else:
  454.             raise ValueError('%s is not a container item' % self)
  455.  
  456.     
  457.     def setFeed(self, feed_id):
  458.         self.feed_id = feed_id
  459.         del self._feed
  460.         if self.isContainerItem:
  461.             for item in self.getChildren():
  462.                 del item._feed
  463.                 item.signalChange()
  464.             
  465.         
  466.         self.signalChange()
  467.  
  468.     
  469.     def executeExpire(self):
  470.         self.confirmDBThread()
  471.         self.removeFromPlaylists()
  472.         UandA = self.getUandA()
  473.         if not self.isExternal():
  474.             self.deleteFiles()
  475.         
  476.         self.expired = True
  477.         if self.isContainerItem:
  478.             for item in self.getChildren():
  479.                 item.remove()
  480.             
  481.         
  482.         self.isContainerItem = None
  483.         self.isVideo = False
  484.         self.videoFilename = FilenameType('')
  485.         self.seen = self.keep = self.pendingManualDL = False
  486.         self.watchedTime = None
  487.         self.duration = None
  488.         if self.screenshot:
  489.             
  490.             try:
  491.                 os.remove(self.screenshot)
  492.  
  493.         
  494.         self.screenshot = None
  495.         if self.isExternal():
  496.             if self.isDownloaded():
  497.                 new_item = FileItem(self.getVideoFilename(), feed_id = self.feed_id, parent_id = self.parent_id, deleted = True)
  498.                 if self.downloader is not None:
  499.                     self.downloader.setDeleteFiles(False)
  500.                 
  501.             
  502.             self.remove()
  503.         else:
  504.             self.signalChange()
  505.  
  506.     
  507.     def expire(self):
  508.         title = _('Removing %s') % os.path.basename(self.getTitle())
  509.         if self.isExternal():
  510.             if self.isContainerItem:
  511.                 description = _('Would you like to delete this folder and all of its videos or just remove its entry from the Library?')
  512.                 button = dialogs.BUTTON_DELETE_FILES
  513.             elif self.isDownloaded():
  514.                 description = _('Would you like to delete this file or just remove its entry from the Library?')
  515.                 button = dialogs.BUTTON_DELETE_FILE
  516.             else:
  517.                 self.executeExpire()
  518.                 return None
  519.             d = dialogs.ThreeChoiceDialog(title, description, dialogs.BUTTON_REMOVE_ENTRY, button, dialogs.BUTTON_CANCEL)
  520.             
  521.             def callback(dialog):
  522.                 if not self.idExists():
  523.                     return None
  524.                 
  525.                 if dialog.choice == button:
  526.                     self.deleteFiles()
  527.                 
  528.                 if dialog.choice in (button, dialogs.BUTTON_REMOVE_ENTRY):
  529.                     self.executeExpire()
  530.                 
  531.  
  532.             d.run(callback)
  533.         elif self.isContainerItem:
  534.             description = _('This item is a folder.  When you remove a folder, any items inside that folder will be deleted.')
  535.             d = dialogs.ChoiceDialog(title, description, dialogs.BUTTON_DELETE_FILES, dialogs.BUTTON_CANCEL)
  536.             
  537.             def callback(dialog):
  538.                 if self.idExists() and dialog.choice == dialogs.BUTTON_DELETE_FILES:
  539.                     self.executeExpire()
  540.                 
  541.  
  542.             d.run(callback)
  543.         else:
  544.             self.executeExpire()
  545.  
  546.     
  547.     def stopUpload(self):
  548.         if self.downloader:
  549.             self.downloader.stopUpload()
  550.         
  551.  
  552.     
  553.     def startUpload(self):
  554.         if self.downloader:
  555.             self.downloader.startUpload()
  556.         
  557.  
  558.     
  559.     def getString(self, when):
  560.         '''Get the expiration time a string to display to the user.'''
  561.         offset = when - datetime.now()
  562.         if offset.days > 0:
  563.             result = _('%d days') % offset.days
  564.         elif offset.seconds > 3600:
  565.             result = _('%d hours') % ceil(offset.seconds / 3600)
  566.         else:
  567.             result = _('%d minutes') % ceil(offset.seconds / 60)
  568.         return result
  569.  
  570.     getString = returnsUnicode(getString)
  571.     
  572.     def getExpirationString(self):
  573.         '''Get the expiration time a string to display to the user.'''
  574.         expireTime = self.getExpirationTime()
  575.         if expireTime is None:
  576.             return u''
  577.         else:
  578.             return _('Expires in %s') % self.getString(expireTime)
  579.  
  580.     getExpirationString = returnsUnicode(getExpirationString)
  581.     
  582.     def getPausedString(self):
  583.         '''Get the expiration time a string to display to the user.'''
  584.         retryTime = None
  585.         if self.downloader:
  586.             if self.downloader.getState() == u'offline':
  587.                 retryTime = self.downloader.status['retryTime']
  588.                 if retryTime is None:
  589.                     return ''
  590.                 else:
  591.                     return _('Will retry in %s') % self.getString(retryTime)
  592.             else:
  593.                 return _('Paused')
  594.         else:
  595.             return u''
  596.  
  597.     getPausedString = returnsUnicode(getPausedString)
  598.     
  599.     def getDragType(self):
  600.         if self.isDownloaded():
  601.             return u'downloadeditem'
  602.         else:
  603.             return u'item'
  604.  
  605.     getDragType = returnsUnicode(getDragType)
  606.     
  607.     def getEmblemCSSClass(self):
  608.         if self.getState() == u'newly-downloaded':
  609.             return u'newly-downloaded'
  610.         elif self.getState() == u'new':
  611.             return u'new'
  612.         else:
  613.             return u''
  614.  
  615.     getEmblemCSSClass = returnsUnicode(getEmblemCSSClass)
  616.     
  617.     def getEmblemCSSString(self):
  618.         if self.getState() == u'newly-downloaded':
  619.             return u'UNWATCHED'
  620.         elif self.getState() == u'new':
  621.             return u'NEW'
  622.         else:
  623.             return u''
  624.  
  625.     getEmblemCSSString = returnsUnicode(getEmblemCSSString)
  626.     
  627.     def getUandA(self):
  628.         '''Get whether this item is new, or newly-downloaded, or neither.'''
  629.         state = self.getState()
  630.         if state == u'new':
  631.             return (0, 1)
  632.         elif state == u'newly-downloaded':
  633.             return (1, 0)
  634.         else:
  635.             return (0, 0)
  636.  
  637.     
  638.     def getExpirationTime(self):
  639.         """Get the time when this item will expire. 
  640.         Returns a datetime object,  or None if it doesn't expire.
  641.         """
  642.         self.confirmDBThread()
  643.         if self.getWatchedTime() is None or not self.isDownloaded():
  644.             return None
  645.         
  646.         ufeed = self.getFeed()
  647.         if (ufeed.expire == u'never' or ufeed.expire == u'system') and config.get(prefs.EXPIRE_AFTER_X_DAYS) <= 0:
  648.             return None
  649.         elif ufeed.expire == u'feed':
  650.             expireTime = ufeed.expireTime
  651.         elif ufeed.expire == u'system':
  652.             expireTime = timedelta(days = config.get(prefs.EXPIRE_AFTER_X_DAYS))
  653.         
  654.         return self.getWatchedTime() + expireTime
  655.  
  656.     
  657.     def getWatchedTime(self):
  658.         if not self.getSeen():
  659.             return None
  660.         
  661.         if self.isContainerItem and self.watchedTime == None:
  662.             self.watchedTime = datetime.min
  663.             for item in self.getChildren():
  664.                 childTime = item.getWatchedTime()
  665.                 if childTime is None:
  666.                     self.watchedTime = None
  667.                     return None
  668.                 
  669.                 if childTime > self.watchedTime:
  670.                     self.watchedTime = childTime
  671.                     continue
  672.             
  673.             self.signalChange()
  674.         
  675.         return self.watchedTime
  676.  
  677.     
  678.     def getExpiring(self):
  679.         if self.expiring is None:
  680.             if not self.getSeen():
  681.                 self.expiring = False
  682.             else:
  683.                 ufeed = self.getFeed()
  684.                 if (self.keep and ufeed.expire == u'never' or ufeed.expire == u'system') and config.get(prefs.EXPIRE_AFTER_X_DAYS) <= 0:
  685.                     self.expiring = False
  686.                 else:
  687.                     self.expiring = True
  688.         
  689.         return self.expiring
  690.  
  691.     
  692.     def getSeen(self):
  693.         self.confirmDBThread()
  694.         if self.isContainerItem:
  695.             if self.childrenSeen is None:
  696.                 self.childrenSeen = True
  697.                 for item in self.getChildren():
  698.                     if not item.seen:
  699.                         self.childrenSeen = False
  700.                         break
  701.                         continue
  702.                 
  703.             
  704.             return self.childrenSeen
  705.         else:
  706.             return self.seen
  707.  
  708.     
  709.     def markItemSeen(self):
  710.         self.confirmDBThread()
  711.         if self.seen == False:
  712.             self.seen = True
  713.             if self.watchedTime is None:
  714.                 self.watchedTime = datetime.now()
  715.             
  716.             self.clearParentsChildrenSeen()
  717.             self.signalChange()
  718.         
  719.  
  720.     
  721.     def clearParentsChildrenSeen(self):
  722.         if self.parent_id:
  723.             parent = self.getParent()
  724.             parent.childrenSeen = None
  725.             parent.signalChange()
  726.         
  727.  
  728.     
  729.     def markItemUnseen(self):
  730.         self.confirmDBThread()
  731.         if self.isContainerItem:
  732.             self.childrenSeen = False
  733.             for item in self.getChildren():
  734.                 item.seen = False
  735.                 item.signalChange()
  736.             
  737.             self.signalChange()
  738.         elif self.seen == False:
  739.             return None
  740.         
  741.         self.seen = False
  742.         self.watchedTime = None
  743.         self.clearParentsChildrenSeen()
  744.         self.signalChange()
  745.  
  746.     
  747.     def getRSSID(self):
  748.         self.confirmDBThread()
  749.         return self.entry['id']
  750.  
  751.     getRSSID = returnsUnicode(getRSSID)
  752.     
  753.     def removeRSSID(self):
  754.         self.confirmDBThread()
  755.         if 'id' in self.entry:
  756.             del self.entry['id']
  757.             self.signalChange()
  758.         
  759.  
  760.     
  761.     def setAutoDownloaded(self, autodl = True):
  762.         self.confirmDBThread()
  763.         if autodl != self.autoDownloaded:
  764.             self.autoDownloaded = autodl
  765.             self.signalChange()
  766.         
  767.  
  768.     
  769.     def setResumeTime(self, position):
  770.         if not self.idExists():
  771.             return None
  772.         
  773.         position = int(position)
  774.         if self.resumeTime != position:
  775.             self.resumeTime = position
  776.             self.signalChange()
  777.         
  778.  
  779.     setResumeTime = eventloop.asIdle(setResumeTime)
  780.     
  781.     def getPendingReason(self):
  782.         self.confirmDBThread()
  783.         return self.pendingReason
  784.  
  785.     getPendingReason = returnsUnicode(getPendingReason)
  786.     
  787.     def getAutoDownloaded(self):
  788.         self.confirmDBThread()
  789.         return self.autoDownloaded
  790.  
  791.     
  792.     def getLinkNumber(self):
  793.         self.confirmDBThread()
  794.         return self.linkNumber
  795.  
  796.     
  797.     def download(self, autodl = False):
  798.         autodler.resumeDownloader()
  799.         self.confirmDBThread()
  800.         manualDownloadCount = views.manualDownloads.len()
  801.         self.expired = self.keep = self.seen = False
  802.         if not autodl and manualDownloadCount >= config.get(prefs.MAX_MANUAL_DOWNLOADS):
  803.             self.pendingManualDL = True
  804.             self.pendingReason = u'queued for download'
  805.             self.signalChange()
  806.             return None
  807.         else:
  808.             self.setAutoDownloaded(autodl)
  809.             self.pendingManualDL = False
  810.         if self.downloader is None:
  811.             self.downloader = downloader.getDownloader(self)
  812.         
  813.         if self.downloader is not None:
  814.             self.downloader.setChannelName(platformutils.unicodeToFilename(self.getChannelTitle(True)))
  815.             if self.downloader.isFinished():
  816.                 self.onDownloadFinished()
  817.             else:
  818.                 self.downloader.start()
  819.         
  820.         self.signalChange()
  821.  
  822.     
  823.     def pause(self):
  824.         if self.downloader:
  825.             self.downloader.pause()
  826.         
  827.  
  828.     
  829.     def resume(self):
  830.         self.download(self.getAutoDownloaded())
  831.  
  832.     
  833.     def isPendingManualDownload(self):
  834.         self.confirmDBThread()
  835.         return self.pendingManualDL
  836.  
  837.     
  838.     def isEligibleForAutoDownload(self):
  839.         self.confirmDBThread()
  840.         if self.getState() not in (u'new', u'not-downloaded'):
  841.             return False
  842.         
  843.         if self.downloader and self.downloader.getState() in (u'failed', u'stopped', u'paused'):
  844.             return False
  845.         
  846.         ufeed = self.getFeed()
  847.         if ufeed.getEverything:
  848.             return True
  849.         
  850.         return self.eligibleForAutoDownload
  851.  
  852.     
  853.     def isPendingAutoDownload(self):
  854.         if self.getFeed().isAutoDownloadable():
  855.             pass
  856.         return self.isEligibleForAutoDownload()
  857.  
  858.     
  859.     def isFailedDownload(self):
  860.         if self.downloader:
  861.             pass
  862.         return self.downloader.getState() == u'failed'
  863.  
  864.     
  865.     def getThumbnailURL(self):
  866.         self.confirmDBThread()
  867.         videoEnclosure = self.getFirstVideoEnclosure()
  868.         if videoEnclosure is not None:
  869.             
  870.             try:
  871.                 return videoEnclosure['thumbnail']['url'].decode('ascii', 'replace')
  872.  
  873.         
  874.         for enclosure in self.entry.enclosures:
  875.             
  876.             try:
  877.                 return enclosure['thumbnail']['url'].decode('ascii', 'replace')
  878.             continue
  879.             except KeyError:
  880.                 continue
  881.             
  882.  
  883.         
  884.         
  885.         try:
  886.             return self.entry['thumbnail']['url'].decode('ascii', 'replace')
  887.         except:
  888.             None<EXCEPTION MATCH>KeyError
  889.             return None
  890.  
  891.  
  892.     getThumbnailURL = returnsUnicode(getThumbnailURL)
  893.     
  894.     def getThumbnail(self):
  895.         self.confirmDBThread()
  896.         if self.showMoreInfo:
  897.             (width, height) = Item.BIG_ICON_SIZE
  898.         else:
  899.             (width, height) = Item.SMALL_ICON_SIZE
  900.         if self.iconCache.isValid():
  901.             path = self.iconCache.getResizedFilename(width, height)
  902.             return resources.absoluteUrl(path)
  903.         elif self.screenshot:
  904.             path = self.getResizedScreenshot(width, height)
  905.             return resources.absoluteUrl(path)
  906.         elif self.isContainerItem:
  907.             return resources.url(u'images/container-icon.png')
  908.         else:
  909.             feedThumbnail = self.getFeed().getItemThumbnail(width, height)
  910.             if feedThumbnail is not None:
  911.                 return feedThumbnail
  912.             elif self.showMoreInfo:
  913.                 return resources.url(u'images/thumb-more-info.png')
  914.             else:
  915.                 return resources.url(u'images/thumb.png')
  916.  
  917.     getThumbnail = returnsUnicode(getThumbnail)
  918.     
  919.     def getTitle(self):
  920.         
  921.         try:
  922.             return self.entry.title
  923.         except:
  924.             
  925.             try:
  926.                 enclosure = self.getFirstVideoEnclosure()
  927.                 return enclosure['url'].decode('ascii', 'replace')
  928.             return u''
  929.  
  930.  
  931.  
  932.     getTitle = returnsUnicode(getTitle)
  933.     
  934.     def getQuotedTitle(self):
  935.         return urllib.quote_plus(self.getTitle().encode('utf8')).decode('ascii', 'replace')
  936.  
  937.     getQuotedTitle = returnsUnicode(getQuotedTitle)
  938.     
  939.     def getChannelTitle(self, allowSearchFeedTitle = False):
  940.         implClass = self.getFeed().actualFeed.__class__
  941.         if implClass in (feed.RSSFeedImpl, feed.ScraperFeedImpl):
  942.             return self.getFeed().getTitle()
  943.         elif implClass == feed.SearchFeedImpl and allowSearchFeedTitle:
  944.             return searchengines.getLastEngineTitle()
  945.         else:
  946.             return u''
  947.  
  948.     getChannelTitle = returnsUnicode(getChannelTitle)
  949.     
  950.     def getRawDescription(self):
  951.         self.confirmDBThread()
  952.         
  953.         try:
  954.             enclosure = self.getFirstVideoEnclosure()
  955.             return enclosure['text']
  956.         except:
  957.             
  958.             try:
  959.                 return self.entry.description
  960.             return u''
  961.  
  962.  
  963.  
  964.     getRawDescription = returnsUnicode(getRawDescription)
  965.     
  966.     def getDescription(self):
  967.         rawDescription = self.getRawDescription()
  968.         
  969.         try:
  970.             purifiedDescription = adscraper.purify(rawDescription)
  971.             return xhtmlify(u'<span>%s</span>' % (unescape(purifiedDescription),), filterFontTags = True)
  972.         except:
  973.             
  974.             try:
  975.                 return xhtmlify(u'<span>%s</span>' % (unescape(rawDescription),))
  976.             return u'<span />'
  977.  
  978.  
  979.  
  980.     getDescription = returnsUnicode(getDescription)
  981.     
  982.     def getAd(self):
  983.         rawDescription = self.getRawDescription()
  984.         
  985.         try:
  986.             rawAd = adscraper.scrape(rawDescription)
  987.             return xhtmlify(u'<span>%s</span>' % (unescape(rawAd),))
  988.         except:
  989.             return u'<span />'
  990.  
  991.  
  992.     
  993.     def looksLikeTorrent(self):
  994.         """Returns true if we think this item is a torrent.  (For items that
  995.         haven't been downloaded this uses the file extension which isn't
  996.         totally reliable).
  997.         """
  998.         if self.downloader is not None:
  999.             return self.downloader.getType() == u'bittorrent'
  1000.         else:
  1001.             return self.getURL().endswith(u'.torrent')
  1002.  
  1003.     
  1004.     def getDetails(self):
  1005.         details = []
  1006.         reldate = self.getReleaseDate()
  1007.         format = self.getFormat()
  1008.         size = self.getSizeForDisplay()
  1009.         link = self.getLink()
  1010.         if self.isContainerItem:
  1011.             children = self.getChildren()
  1012.             details.append(u'<span class="details-count">%s items</span>' % len(children))
  1013.         
  1014.         if len(reldate) > 0:
  1015.             details.append(u'<span class="details-date">%s</span>' % escape(reldate))
  1016.         
  1017.         if len(size) > 0:
  1018.             details.append(u'<span class="details-size">%s</span>' % escape(size))
  1019.         
  1020.         if len(format) > 0:
  1021.             details.append(u'<span class="details-format">%s</span>' % escape(format))
  1022.         
  1023.         if self.looksLikeTorrent():
  1024.             details.append(u'<span class="details-torrent">%s</span>' % _('TORRENT'))
  1025.         
  1026.         if len(link) > 0 and link != self.getURL():
  1027.             details.append(u'<a class="details-link" href="%s">%s</span>' % (quoteattr(link), _('WEB PAGE')))
  1028.         
  1029.         out = u'<BR>'.join(details)
  1030.         return out
  1031.  
  1032.     getDetails = returnsUnicode(getDetails)
  1033.     
  1034.     def isTransferring(self):
  1035.         if self.downloader:
  1036.             pass
  1037.         return self.downloader.getState() in (u'uploading', u'downloading')
  1038.  
  1039.     
  1040.     def getDownloadDetails(self):
  1041.         status = self.downloader.status
  1042.         details = [
  1043.             (_('Total Down:'), formatSizeForDetails(status.get('currentSize', 0)))]
  1044.         if status.get('reasonFailed'):
  1045.             details.append((_('Error:'), status['reasonFailed']))
  1046.         
  1047.         return details
  1048.  
  1049.     
  1050.     def getTorrentDetails(self):
  1051.         status = self.downloader.status
  1052.         return [
  1053.             (_('Down Rate:'), formatRateForDetails(status.get('rate', 0))),
  1054.             (_('Down Total:'), formatSizeForDetails(status.get('currentSize', 0))),
  1055.             (_('Up Rate:'), formatRateForDetails(status.get('upRate', 0))),
  1056.             (_('Up Total:'), formatSizeForDetails(status.get('uploaded', 0) * 1024 * 1024))]
  1057.  
  1058.     
  1059.     def getItemDetails(self):
  1060.         rv = []
  1061.         link = self.getLink()
  1062.         if link:
  1063.             rv.append((_('Web page:'), util.makeAnchor(_('permalink'), link)))
  1064.         
  1065.         url = self.getURL()
  1066.         if url and not url.startswith('file:'):
  1067.             rv.append((_('File link:'), util.makeAnchor(_('direct link to file'), url)))
  1068.         
  1069.         rv.append((_('File type:'), self.getFormat()))
  1070.         if self.isDownloaded():
  1071.             basename = os.path.basename(self.getFilename())
  1072.             basename = util.clampText(basename, 40)
  1073.             linkEventURL = u'revealItem?item=%d' % self.getID()
  1074.             if self.isContainerItem:
  1075.                 label = _('REVEAL LOCAL FOLDER')
  1076.             else:
  1077.                 label = _('REVEAL LOCAL FILE')
  1078.             link = util.makeEventURL(label, linkEventURL)
  1079.             rv.append((_('Filename:'), u'%s<BR />%s' % (platformutils.filenameToUnicode(basename), link)))
  1080.         
  1081.         return rv
  1082.  
  1083.     
  1084.     def getTorrentDetailsFinished(self):
  1085.         status = self.downloader.status
  1086.         return [
  1087.             (_('Down Total'), formatSizeForDetails(status.get('currentSize', 0))),
  1088.             (_('Up Total'), formatSizeForDetails(status.get('uploaded', 0) * 1024 * 1024))]
  1089.  
  1090.     
  1091.     def makeMoreInfoTable(self, title, moreInfoData):
  1092.         lines = []
  1093.         lines.append(u'<h3>%s</h3>' % title)
  1094.         lines.append(u'<table cellpadding="0" cellspacing="0">')
  1095.         for label, text in moreInfoData:
  1096.             lines.append(u'<tr><td class="label">%s</td><td class="value">%s</td></tr>' % (label, text))
  1097.         
  1098.         lines.append(u'</table>')
  1099.         return u'\n'.join(lines)
  1100.  
  1101.     
  1102.     def getMoreInfo(self):
  1103.         details = [
  1104.             self.makeMoreInfoTable(_('Item Details'), self.getItemDetails())]
  1105.         
  1106.         def addTable(label, data):
  1107.             details.append(self.makeMoreInfoTable(label, data))
  1108.  
  1109.         if self.looksLikeTorrent():
  1110.             if self.isTransferring():
  1111.                 addTable(_('Torrent Details'), self.getTorrentDetails())
  1112.             elif self.downloader and self.downloader.isFinished():
  1113.                 addTable(_('Torrent Details <i>stopped</i>'), self.getTorrentDetailsFinished())
  1114.             
  1115.         elif self.getState() == u'downloading' or not (self.pendingManualDL) or self.isFailedDownload():
  1116.             addTable(_('Download Details'), self.getDownloadDetails())
  1117.         
  1118.         return u'\n'.join(details)
  1119.  
  1120.     getMoreInfo = returnsUnicode(getMoreInfo)
  1121.     
  1122.     def deleteFiles(self):
  1123.         self.confirmDBThread()
  1124.         if self.downloader is not None:
  1125.             self.downloader.removeItem(self)
  1126.             self.downloader = None
  1127.             self.signalChange()
  1128.         
  1129.  
  1130.     
  1131.     def getState(self):
  1132.         '''Get the state of this item.  The state will be on of the following:
  1133.  
  1134.         * new -- User has never seen this item
  1135.         * not-downloaded -- User has seen the item, but not downloaded it
  1136.         * downloading -- Item is currently downloading
  1137.         * newly-downloaded -- Item has been downoladed, but not played
  1138.         * expiring -- Item has been played and is set to expire
  1139.         * saved -- Item has been played and has been saved
  1140.         * expired -- Item has expired.
  1141.  
  1142.         Uses caching to prevent recalculating state over and over
  1143.         '''
  1144.         
  1145.         try:
  1146.             return self._state
  1147.         except AttributeError:
  1148.             self._calcState()
  1149.             return self._state
  1150.  
  1151.  
  1152.     
  1153.     def _calcState(self):
  1154.         self.confirmDBThread()
  1155.         if self.downloader is None or self.downloader.getState() in (u'failed', u'stopped'):
  1156.             if self.pendingManualDL:
  1157.                 self._state = u'downloading'
  1158.             elif self.expired:
  1159.                 self._state = u'expired'
  1160.             elif (self.getViewed() or self.downloader) and self.downloader.getState() in (u'failed', u'stopped'):
  1161.                 self._state = u'not-downloaded'
  1162.             else:
  1163.                 self._state = u'new'
  1164.         elif self.downloader.getState() in (u'offline', u'paused'):
  1165.             if self.pendingManualDL:
  1166.                 self._state = u'downloading'
  1167.             else:
  1168.                 self._state = u'paused'
  1169.         elif not self.downloader.isFinished():
  1170.             self._state = u'downloading'
  1171.         elif not self.getSeen():
  1172.             self._state = u'newly-downloaded'
  1173.         elif self.getExpiring():
  1174.             self._state = u'expiring'
  1175.         else:
  1176.             self._state = u'saved'
  1177.  
  1178.     _calcState = returnsUnicode(_calcState)
  1179.     
  1180.     def getChannelCategory(self):
  1181.         """Get the category to use for the channel template.  
  1182.         
  1183.         This method is similar to getState(), but has some subtle differences.
  1184.         getState() is used by the download-item template and is usually more
  1185.         useful to determine what's actually happening with an item.
  1186.         getChannelCategory() is used by by the channel template to figure out
  1187.         which heading to put an item under.
  1188.  
  1189.         * downloading and not-downloaded are grouped together as
  1190.           not-downloaded
  1191.         * Newly downloaded and downloading items are always new if
  1192.           their feed hasn't been marked as viewed after the item's pub
  1193.           date.  This is so that when a user gets a list of items and
  1194.           starts downloading them, the list doesn't reorder itself.
  1195.           Once they start watching them, then it reorders itself.
  1196.         """
  1197.         self.confirmDBThread()
  1198.         if self.downloader is None or not self.downloader.isFinished():
  1199.             if not self.getViewed():
  1200.                 return u'new'
  1201.             
  1202.             if self.expired:
  1203.                 return u'expired'
  1204.             else:
  1205.                 return u'not-downloaded'
  1206.         elif not self.getSeen():
  1207.             if not self.getViewed():
  1208.                 return u'new'
  1209.             
  1210.             return u'newly-downloaded'
  1211.         elif self.getExpiring():
  1212.             return u'expiring'
  1213.         else:
  1214.             return u'saved'
  1215.  
  1216.     getChannelCategory = returnsUnicode(getChannelCategory)
  1217.     
  1218.     def isDownloadable(self):
  1219.         return self.getState() in (u'new', u'not-downloaded', u'expired')
  1220.  
  1221.     
  1222.     def isDownloaded(self):
  1223.         return self.getState() in (u'newly-downloaded', u'expiring', u'saved')
  1224.  
  1225.     
  1226.     def showSaveButton(self):
  1227.         if self.getState() in (u'newly-downloaded', u'expiring'):
  1228.             pass
  1229.         return not (self.keep)
  1230.  
  1231.     
  1232.     def showSaved(self):
  1233.         if self.getState() in (u'saved',) and self.getState() in (u'newly-downloaded', u'expiring'):
  1234.             pass
  1235.         return self.keep
  1236.  
  1237.     
  1238.     def showTrashButton(self):
  1239.         if self.isDownloaded() and self.getFeedURL() == u'dtv:manualFeed':
  1240.             pass
  1241.         return self.getState() not in (u'downloading', u'paused')
  1242.  
  1243.     
  1244.     def getFailureReason(self):
  1245.         self.confirmDBThread()
  1246.         if self.downloader is not None:
  1247.             return self.downloader.getShortReasonFailed()
  1248.         else:
  1249.             return u''
  1250.  
  1251.     getFailureReason = returnsUnicode(getFailureReason)
  1252.     
  1253.     def getSizeForDisplay(self):
  1254.         return util.formatSizeForUser(self.getSize())
  1255.  
  1256.     
  1257.     def getSize(self):
  1258.         if not hasattr(self, '_size'):
  1259.             self._size = self._getSize()
  1260.         
  1261.         return self._size
  1262.  
  1263.     
  1264.     def _getSize(self):
  1265.         fname = self.getFilename()
  1266.         if self.isDownloaded():
  1267.             
  1268.             try:
  1269.                 return util.getsize(fname)
  1270.             except OSError:
  1271.                 return 0
  1272.             except:
  1273.                 None<EXCEPTION MATCH>OSError
  1274.             
  1275.  
  1276.         None<EXCEPTION MATCH>OSError
  1277.         if self.downloader is not None:
  1278.             return self.downloader.getTotalSize()
  1279.         else:
  1280.             
  1281.             try:
  1282.                 return int(self.getFirstVideoEnclosure()['length'])
  1283.             except:
  1284.                 return 0
  1285.  
  1286.  
  1287.     
  1288.     def getCurrentSize(self):
  1289.         if self.downloader is not None:
  1290.             size = self.downloader.getCurrentSize()
  1291.         else:
  1292.             size = 0
  1293.         return util.formatSizeForUser(size)
  1294.  
  1295.     getCurrentSize = returnsUnicode(getCurrentSize)
  1296.     
  1297.     def downloadProgress(self):
  1298.         progress = 0
  1299.         self.confirmDBThread()
  1300.         if self.downloader is None:
  1301.             return 0
  1302.         else:
  1303.             size = self.downloader.getTotalSize()
  1304.             dled = self.downloader.getCurrentSize()
  1305.             if size == 0:
  1306.                 return 0
  1307.             else:
  1308.                 return 100 * dled / size
  1309.  
  1310.     
  1311.     def gotContentLength(self):
  1312.         if self.downloader is None:
  1313.             return False
  1314.         else:
  1315.             return self.downloader.getTotalSize() != -1
  1316.  
  1317.     
  1318.     def downloadProgressWidth(self):
  1319.         fullWidth = 112
  1320.         progress = self.downloadProgress() / 100
  1321.         if progress == 0:
  1322.             return 0
  1323.         
  1324.         return int(progress * fullWidth)
  1325.  
  1326.     
  1327.     def threeDigitPercentDone(self):
  1328.         return u'%03d' % int(self.downloadProgress())
  1329.  
  1330.     threeDigitPercentDone = returnsUnicode(threeDigitPercentDone)
  1331.     
  1332.     def downloadInProgress(self):
  1333.         if self.downloader is not None:
  1334.             pass
  1335.         return self.downloader.getETA() != 0
  1336.  
  1337.     
  1338.     def downloadETA(self):
  1339.         if self.downloader is not None:
  1340.             totalSecs = self.downloader.getETA()
  1341.             if totalSecs == -1:
  1342.                 return _('downloading...')
  1343.             
  1344.         else:
  1345.             totalSecs = 0
  1346.         (mins, secs) = divmod(totalSecs, 60)
  1347.         (hours, mins) = divmod(mins, 60)
  1348.         if hours > 0:
  1349.             time = u'%d:%02d:%02d' % (hours, mins, secs)
  1350.             return _('%s remaining') % time
  1351.         else:
  1352.             time = u'%d:%02d' % (mins, secs)
  1353.             return _('%s remaining') % time
  1354.  
  1355.     downloadETA = returnsUnicode(downloadETA)
  1356.     
  1357.     def getStartupActivity(self):
  1358.         if self.pendingManualDL:
  1359.             return self.pendingReason
  1360.         elif self.downloader:
  1361.             return self.downloader.getStartupActivity()
  1362.         else:
  1363.             return _('starting up...')
  1364.  
  1365.     getStartupActivity = returnsUnicode(getStartupActivity)
  1366.     
  1367.     def downloadRate(self):
  1368.         rate = 0
  1369.         unit = u'KB/s'
  1370.         if self.downloader is not None:
  1371.             rate = self.downloader.getRate()
  1372.         else:
  1373.             rate = 0
  1374.         rate /= 1024
  1375.         if rate > 1024:
  1376.             rate /= 1024
  1377.             unit = u'MB/s'
  1378.         
  1379.         if rate > 1024:
  1380.             rate /= 1024
  1381.             unit = u'GB/s'
  1382.         
  1383.         return u'%d%s' % (rate, unit)
  1384.  
  1385.     downloadRate = returnsUnicode(downloadRate)
  1386.     
  1387.     def getPubDate(self):
  1388.         return getReleaseDate()
  1389.  
  1390.     getPubDate = returnsUnicode(getPubDate)
  1391.     
  1392.     def getPubDateParsed(self):
  1393.         return self.getReleaseDateObj()
  1394.  
  1395.     
  1396.     def getReleaseDate(self):
  1397.         
  1398.         try:
  1399.             return self.getReleaseDateObj().strftime('%b %d %Y').decode(_charset)
  1400.         except:
  1401.             return u''
  1402.  
  1403.  
  1404.     getReleaseDate = returnsUnicode(getReleaseDate)
  1405.     
  1406.     def getReleaseDateObj(self):
  1407.         return self.releaseDateObj
  1408.  
  1409.     
  1410.     def getDurationValue(self):
  1411.         secs = 0
  1412.         if self.duration not in (-1, None):
  1413.             secs = self.duration / 1000
  1414.         
  1415.         return secs
  1416.  
  1417.     
  1418.     def getDuration(self, emptyIfZero = True):
  1419.         secs = self.getDurationValue()
  1420.         if secs == 0:
  1421.             if emptyIfZero:
  1422.                 return u''
  1423.             else:
  1424.                 return 'n/a'
  1425.         
  1426.         return u'%02d:%02d' % (secs / 60, secs % 60)
  1427.  
  1428.     getDuration = returnsUnicode(getDuration)
  1429.     KNOWN_MIME_TYPES = (u'audio', u'video')
  1430.     KNOWN_MIME_SUBTYPES = (u'mov', u'wmv', u'mp4', u'mp3', u'mpg', u'mpeg', u'avi', u'x-flv', u'x-msvideo', u'm4v', u'mkv', u'm2v')
  1431.     MIME_SUBSITUTIONS = {
  1432.         u'QUICKTIME': u'MOV' }
  1433.     
  1434.     def getFormat(self, emptyForUnknown = True):
  1435.         if self.looksLikeTorrent():
  1436.             return u'.torrent'
  1437.         
  1438.         
  1439.         try:
  1440.             enclosure = self.entry['enclosures'][0]
  1441.             
  1442.             try:
  1443.                 extension = enclosure['url'].split('.')[-1].lower().decode('ascii', 'replace')
  1444.             except:
  1445.                 extension == u''
  1446.  
  1447.             if extension.lower() == u'mp3':
  1448.                 return u'.mp3'
  1449.             
  1450.             if enclosure.has_key('type') and len(enclosure['type']) > 0:
  1451.                 (mtype, subtype) = enclosure['type'].decode('ascii', 'replace').split('/')
  1452.                 mtype = mtype.lower()
  1453.                 if mtype in self.KNOWN_MIME_TYPES:
  1454.                     format = subtype.split(';')[0].upper()
  1455.                     if mtype == u'audio':
  1456.                         format += u' AUDIO'
  1457.                     
  1458.                     if format.startswith(u'X-'):
  1459.                         format = format[2:]
  1460.                     
  1461.                     return u'.%s' % self.MIME_SUBSITUTIONS.get(format, format).lower()
  1462.                 
  1463.             
  1464.             if extension in self.KNOWN_MIME_SUBTYPES:
  1465.                 return u'.%s' % extension
  1466.         except:
  1467.             pass
  1468.  
  1469.         if emptyForUnknown:
  1470.             return u''
  1471.         else:
  1472.             return u'unknown'
  1473.  
  1474.     getFormat = returnsUnicode(getFormat)
  1475.     
  1476.     def getTags(self):
  1477.         self.confirmDBThread()
  1478.         
  1479.         try:
  1480.             return self.entry.categories.join(u', ')
  1481.         except:
  1482.             return u''
  1483.  
  1484.  
  1485.     getTags = returnsUnicode(getTags)
  1486.     
  1487.     def getLicence(self):
  1488.         self.confirmDBThread()
  1489.         
  1490.         try:
  1491.             return self.entry.license
  1492.         except:
  1493.             
  1494.             try:
  1495.                 return self.getFeed().getLicense()
  1496.             return u''
  1497.  
  1498.  
  1499.  
  1500.     getLicence = returnsUnicode(getLicence)
  1501.     
  1502.     def getPeople(self):
  1503.         ret = []
  1504.         self.confirmDBThread()
  1505.         
  1506.         try:
  1507.             for role in self.getFirstVideoEnclosure().roles:
  1508.                 for person in self.getFirstVideoEnclosure().roles[role]:
  1509.                     ret.append(person)
  1510.                 
  1511.             
  1512.             for role in self.entry.roles:
  1513.                 for person in self.entry.roles[role]:
  1514.                     ret.append(person)
  1515.                 
  1516.         except:
  1517.             pass
  1518.  
  1519.         return u', '.join(ret)
  1520.  
  1521.     getPeople = returnsUnicode(getPeople)
  1522.     
  1523.     def getLink(self):
  1524.         self.confirmDBThread()
  1525.         
  1526.         try:
  1527.             return self.entry.link.decode('ascii', 'replace')
  1528.         except:
  1529.             return u''
  1530.  
  1531.  
  1532.     
  1533.     def getPaymentLink(self):
  1534.         self.confirmDBThread()
  1535.         
  1536.         try:
  1537.             return self.getFirstVideoEnclosure().payment_url.decode('ascii', 'replace')
  1538.         except:
  1539.             
  1540.             try:
  1541.                 return self.entry.payment_url.decode('ascii', 'replace')
  1542.             return u''
  1543.  
  1544.  
  1545.  
  1546.     
  1547.     def getPaymentHTML(self):
  1548.         self.confirmDBThread()
  1549.         
  1550.         try:
  1551.             ret = self.getFirstVideoEnclosure().payment_html
  1552.         except:
  1553.             
  1554.             try:
  1555.                 ret = self.entry.payment_html
  1556.             ret = u''
  1557.  
  1558.  
  1559.         return u'<span>' + unescape(ret) + u'</span>'
  1560.  
  1561.     getPaymentHTML = returnsUnicode(getPaymentHTML)
  1562.     
  1563.     def makeContextMenu(self, templateName, view):
  1564.         c = app.controller
  1565.         if self.isDownloaded():
  1566.             if templateName in ('playlist', 'playlist-folder'):
  1567.                 label = _('Remove From Playlist')
  1568.             else:
  1569.                 label = _('Remove From the Library')
  1570.             items = [
  1571.                 (None, (None, None, None), ((lambda : c.playView(view, self.getID())), _('Play'))),
  1572.                 ((lambda : c.playView(view, self.getID(), True)), _('Play Just This Video')),
  1573.                 (c.addToNewPlaylist, _('Add to new playlist')),
  1574.                 (c.removeCurrentItems, label)]
  1575.             if self.getSeen():
  1576.                 items.append((self.markItemUnseen, _('Mark as Unwatched')))
  1577.             else:
  1578.                 items.append((self.markItemSeen, _('Mark as Watched')))
  1579.             if self.downloader and self.downloader.getState() == 'finished' and self.downloader.getType() == 'bittorrent':
  1580.                 items.append((self.startUpload, _('Restart Upload')))
  1581.             
  1582.         elif self.getState() == 'downloading':
  1583.             items = [
  1584.                 (self.expire, _('Cancel Download')),
  1585.                 (self.pause, _('Pause Download'))]
  1586.         else:
  1587.             items = [
  1588.                 (self.download, _('Download'))]
  1589.         return menu.makeMenu(items)
  1590.  
  1591.     
  1592.     def update(self, entry):
  1593.         UandA = self.getUandA()
  1594.         self.confirmDBThread()
  1595.         
  1596.         try:
  1597.             self.entry = entry
  1598.             self.iconCache.requestUpdate()
  1599.             self.updateReleaseDate()
  1600.             self._calcFirstEnc()
  1601.         finally:
  1602.             self.signalChange()
  1603.  
  1604.  
  1605.     
  1606.     def onDownloadFinished(self):
  1607.         '''Called when the download for this item finishes.'''
  1608.         self.confirmDBThread()
  1609.         self.downloadedTime = datetime.now()
  1610.         if not self.splitItem():
  1611.             self.signalChange()
  1612.         
  1613.         moviedata.movieDataUpdater.requestUpdate(self)
  1614.         for other in views.items:
  1615.             if other.downloader is None and other.getURL() == self.getURL():
  1616.                 other.downloader = self.downloader
  1617.                 self.downloader.addItem(other)
  1618.                 other.signalChange(needsSave = False)
  1619.                 continue
  1620.         
  1621.         app.delegate.notifyDownloadCompleted(self)
  1622.  
  1623.     
  1624.     def getResizedScreenshot(self, width, height):
  1625.         
  1626.         try:
  1627.             return imageresize.getImage(self.resized_screenshots, width, height)
  1628.         except KeyError:
  1629.             return self.screenshot
  1630.  
  1631.  
  1632.     
  1633.     def resizeScreenshot(self):
  1634.         imageresize.removeResizedFiles(self.resized_screenshots)
  1635.         if self.screenshot:
  1636.             self.resized_screenshots = imageresize.multiResizeImage(self.screenshot, self.ICON_CACHE_SIZES)
  1637.         else:
  1638.             self.resized_screenshots = { }
  1639.  
  1640.     
  1641.     def save(self):
  1642.         self.confirmDBThread()
  1643.         if self.keep != True:
  1644.             self.keep = True
  1645.             self.signalChange()
  1646.         
  1647.  
  1648.     
  1649.     def getDownloadedTime(self):
  1650.         if self.downloadedTime is None:
  1651.             return datetime.min
  1652.         else:
  1653.             return self.downloadedTime
  1654.  
  1655.     
  1656.     def getFilename(self):
  1657.         self.confirmDBThread()
  1658.         
  1659.         try:
  1660.             return self.downloader.getFilename()
  1661.         except:
  1662.             return FilenameType('')
  1663.  
  1664.  
  1665.     getFilename = returnsFilename(getFilename)
  1666.     
  1667.     def getVideoFilename(self):
  1668.         self.confirmDBThread()
  1669.         if self.videoFilename:
  1670.             return os.path.join(self.getFilename(), self.videoFilename)
  1671.         else:
  1672.             return self.getFilename()
  1673.  
  1674.     getVideoFilename = returnsFilename(getVideoFilename)
  1675.     
  1676.     def isNonVideoFile(self):
  1677.         if self.isContainerItem != True:
  1678.             pass
  1679.         return not (self.isVideo)
  1680.  
  1681.     
  1682.     def isExternal(self):
  1683.         '''Returns True iff this item was not downloaded from a Democracy
  1684.         channel.
  1685.         '''
  1686.         if self.feed_id is not None:
  1687.             pass
  1688.         return self.getFeedURL() == 'dtv:manualFeed'
  1689.  
  1690.     
  1691.     def isPlayable(self):
  1692.         '''Returns True iff this item should have a play button.'''
  1693.         if not self.isContainerItem:
  1694.             if self.isDownloaded():
  1695.                 pass
  1696.             return self.getVideoFilename()
  1697.         elif self.isDownloaded():
  1698.             pass
  1699.         return len(self.getChildren()) > 0
  1700.  
  1701.     
  1702.     def getRSSEntry(self):
  1703.         self.confirmDBThread()
  1704.         return self.entry
  1705.  
  1706.     
  1707.     def migrateChildren(self, newdir):
  1708.         if self.isContainerItem:
  1709.             for item in self.getChildren():
  1710.                 item.migrate(newdir)
  1711.             
  1712.         
  1713.  
  1714.     
  1715.     def remove(self):
  1716.         if self.downloader is not None:
  1717.             self.downloader.removeItem(self)
  1718.             self.downloader = None
  1719.         
  1720.         if self.iconCache is not None:
  1721.             self.iconCache.remove()
  1722.             self.iconCache = None
  1723.         
  1724.         imageresize.removeResizedFiles(self.resized_screenshots)
  1725.         if self.isContainerItem:
  1726.             for item in self.getChildren():
  1727.                 item.remove()
  1728.             
  1729.         
  1730.         DDBObject.remove(self)
  1731.  
  1732.     
  1733.     def setupLinks(self):
  1734.         """This is called after we restore the database.  Since we don't store
  1735.         references between objects, we need a way to reconnect downloaders to
  1736.         the items after the restore.
  1737.         """
  1738.         if not isinstance(self, FileItem) and self.downloader is None:
  1739.             self.downloader = downloader.getExistingDownloader(self)
  1740.             if self.downloader is not None:
  1741.                 self.signalChange(needsSave = False)
  1742.             
  1743.         
  1744.         self.splitItem()
  1745.         if self.isContainerItem is not None and not os.path.exists(self.getFilename()):
  1746.             self.executeExpire()
  1747.             return None
  1748.         
  1749.         if self.screenshot and not os.path.exists(self.screenshot):
  1750.             self.screenshot = None
  1751.             self.signalChange()
  1752.         
  1753.         if self.duration is None or self.screenshot is None:
  1754.             moviedata.movieDataUpdater.requestUpdate(self)
  1755.         
  1756.  
  1757.     
  1758.     def __str__(self):
  1759.         return 'Item - %s' % self.getTitle()
  1760.  
  1761.  
  1762.  
  1763. def reconnectDownloaders():
  1764.     reconnected = set()
  1765.     for item in views.items:
  1766.         item.setupLinks()
  1767.         reconnected.add(item.downloader)
  1768.     
  1769.     for downloader in views.remoteDownloads:
  1770.         if downloader not in reconnected:
  1771.             logging.warn('removing orphaned downloader: %s', downloader.url)
  1772.             downloader.remove()
  1773.             continue
  1774.     
  1775.     manualFeed = util.getSingletonDDBObject(views.manualFeed)
  1776.     manualItems = views.items.filterWithIndex(indexes.itemsByFeed, manualFeed.getID())
  1777.     for item in manualItems:
  1778.         if item.downloader is None and item.__class__ == Item:
  1779.             logging.warn('removing cancelled external torrent: %s', item)
  1780.             item.remove()
  1781.             continue
  1782.     
  1783.  
  1784.  
  1785. def getEntryForFile(filename):
  1786.     return FeedParserDict({
  1787.         'title': platformutils.filenameToUnicode(os.path.basename(filename)),
  1788.         'enclosures': [
  1789.             {
  1790.                 'url': resources.url(filename) }] })
  1791.  
  1792.  
  1793. def getEntryForURL(url, contentType = None):
  1794.     if contentType is None:
  1795.         contentType = u'video/x-unknown'
  1796.     else:
  1797.         contentType = unicode(contentType)
  1798.     return FeedParserDict({
  1799.         'title': url,
  1800.         'enclosures': [
  1801.             {
  1802.                 'url': url,
  1803.                 'type': contentType }] })
  1804.  
  1805.  
  1806. class FileItem(Item):
  1807.     
  1808.     def __init__(self, filename, feed_id = None, parent_id = None, offsetPath = None, deleted = False):
  1809.         checkF(filename)
  1810.         filename = os.path.abspath(filename)
  1811.         self.filename = filename
  1812.         self.deleted = deleted
  1813.         self.offsetPath = offsetPath
  1814.         self.shortFilename = cleanFilename(os.path.basename(self.filename))
  1815.         Item.__init__(self, getEntryForFile(filename), feed_id = feed_id, parent_id = parent_id)
  1816.         moviedata.movieDataUpdater.requestUpdate(self)
  1817.  
  1818.     
  1819.     def getState(self):
  1820.         if self.deleted:
  1821.             return u'expired'
  1822.         elif self.getSeen():
  1823.             return u'saved'
  1824.         else:
  1825.             return u'newly-downloaded'
  1826.  
  1827.     getState = returnsUnicode(getState)
  1828.     
  1829.     def getChannelCategory(self):
  1830.         """Get the category to use for the channel template.  
  1831.         
  1832.         This method is similar to getState(), but has some subtle differences.
  1833.         getState() is used by the download-item template and is usually more
  1834.         useful to determine what's actually happening with an item.
  1835.         getChannelCategory() is used by by the channel template to figure out
  1836.         which heading to put an item under.
  1837.  
  1838.         * downloading and not-downloaded are grouped together as
  1839.           not-downloaded
  1840.         * Items are always new if their feed hasn't been marked as viewed
  1841.           after the item's pub date.  This is so that when a user gets a list
  1842.           of items and starts downloading them, the list doesn't reorder
  1843.           itself.
  1844.         * Child items match their parents for expiring, where in
  1845.           getState, they always act as not expiring.
  1846.         """
  1847.         self.confirmDBThread()
  1848.         if self.deleted:
  1849.             return u'expired'
  1850.         elif not self.getSeen():
  1851.             return u'newly-downloaded'
  1852.         elif self.parent_id and self.getParent().getExpiring():
  1853.             return u'expiring'
  1854.         else:
  1855.             return u'saved'
  1856.  
  1857.     
  1858.     def getExpiring(self):
  1859.         return False
  1860.  
  1861.     
  1862.     def showSaveButton(self):
  1863.         return False
  1864.  
  1865.     
  1866.     def getViewed(self):
  1867.         return True
  1868.  
  1869.     
  1870.     def isExternal(self):
  1871.         return self.parent_id is None
  1872.  
  1873.     
  1874.     def executeExpire(self):
  1875.         self.confirmDBThread()
  1876.         self.removeFromPlaylists()
  1877.         if self.isContainerItem:
  1878.             for item in self.getChildren():
  1879.                 item.remove()
  1880.             
  1881.         
  1882.         if not os.path.exists(self.filename):
  1883.             self.remove()
  1884.         elif self.feed_id is None:
  1885.             self.deleted = True
  1886.             self.signalChange()
  1887.         else:
  1888.             url = self.getFeedURL()
  1889.             if url.startswith('dtv:manualFeed') or url.startswith('dtv:singleFeed'):
  1890.                 self.remove()
  1891.             else:
  1892.                 self.deleted = True
  1893.                 self.signalChange()
  1894.  
  1895.     
  1896.     def deleteFiles(self):
  1897.         
  1898.         try:
  1899.             if self.getParent():
  1900.                 dler = self.getParent().downloader
  1901.                 if dler:
  1902.                     dler.stop(False)
  1903.                 
  1904.             
  1905.             if os.path.isfile(self.filename):
  1906.                 os.remove(self.filename)
  1907.             elif os.path.isdir(self.filename):
  1908.                 shutil.rmtree(self.filename)
  1909.         except:
  1910.             logging.warn('WARNING: error deleting files:\n%s', traceback.format_exc())
  1911.  
  1912.  
  1913.     
  1914.     def getDownloadedTime(self):
  1915.         self.confirmDBThread()
  1916.         
  1917.         try:
  1918.             return datetime.fromtimestamp(os.path.getctime(self.filename))
  1919.         except:
  1920.             return datetime.min
  1921.  
  1922.  
  1923.     
  1924.     def getFilename(self):
  1925.         
  1926.         try:
  1927.             return self.filename
  1928.         except:
  1929.             return FilenameType('')
  1930.  
  1931.  
  1932.     getFilename = returnsFilename(getFilename)
  1933.     
  1934.     def download(self, autodl = False):
  1935.         self.deleted = False
  1936.         self.signalChange()
  1937.  
  1938.     
  1939.     def updateReleaseDate(self):
  1940.         
  1941.         try:
  1942.             self.releaseDateObj = datetime.fromtimestamp(os.path.getmtime(self.filename))
  1943.         except:
  1944.             self.releaseDateObj = datetime.min
  1945.  
  1946.  
  1947.     
  1948.     def getReleaseDateObj(self):
  1949.         if self.parent_id:
  1950.             return self.getParent().releaseDateObj
  1951.         else:
  1952.             return self.releaseDateObj
  1953.  
  1954.     
  1955.     def migrate(self, newDir):
  1956.         self.confirmDBThread()
  1957.         if self.parent_id:
  1958.             parent = self.getParent()
  1959.             self.filename = os.path.join(parent.getFilename(), self.offsetPath)
  1960.             return None
  1961.         
  1962.         if self.shortFilename is None:
  1963.             logging.warn("can't migrate download because we don't have a shortFilename!\nfilename was %s", stringify(self.filename))
  1964.             return None
  1965.         
  1966.         newFilename = os.path.join(newDir, self.shortFilename)
  1967.         if self.filename == newFilename:
  1968.             return None
  1969.         
  1970.         if os.path.exists(self.filename):
  1971.             newFilename = nextFreeFilename(newFilename)
  1972.             
  1973.             def callback():
  1974.                 self.filename = newFilename
  1975.                 self.signalChange()
  1976.  
  1977.             fileutil.migrate_file(self.filename, newFilename, callback)
  1978.         elif os.path.exists(newFilename):
  1979.             self.filename = newFilename
  1980.             self.signalChange()
  1981.         
  1982.         self.migrateChildren(newDir)
  1983.  
  1984.     
  1985.     def setupLinks(self):
  1986.         if self.shortFilename is None:
  1987.             if self.parent_id is None:
  1988.                 self.shortFilename = cleanFilename(os.path.basename(self.filename))
  1989.             else:
  1990.                 parent_file = self.getParent().getFilename()
  1991.                 if self.filename.startswith(parent_file):
  1992.                     self.shortFilename = cleanFilename(self.filename[len(parent_file):])
  1993.                 else:
  1994.                     logging.warn('%s is not a subdirectory of %s', self.filename, parent_file)
  1995.         
  1996.         self.updateReleaseDate()
  1997.         Item.setupLinks(self)
  1998.  
  1999.  
  2000.  
  2001. def expireItems(items):
  2002.     if len(items) == 1:
  2003.         return items[0].expire()
  2004.     
  2005.     hasContainers = False
  2006.     hasExternalItems = False
  2007.     for item in items:
  2008.         if item.isContainerItem:
  2009.             hasContainers = True
  2010.         elif item.isExternal():
  2011.             hasExternalItems = True
  2012.         
  2013.         if hasContainers and hasExternalItems:
  2014.             break
  2015.             continue
  2016.     
  2017.     title = _('Removing %s items') % len(items)
  2018.     if hasExternalItems:
  2019.         description = _('One or more of these videos was not downloaded from a channel.  Would you like to delete these items or just remove their entries from the Library?')
  2020.     else:
  2021.         description = u'Are you sure you want to delete all %s videos?' % len(items)
  2022.     if hasContainers:
  2023.         description += u'\n\n' + _('One or more of these items is a folder.  When you remove or delete a folder, any items inside that folder will also be removed or deleted.')
  2024.     
  2025.     if hasExternalItems:
  2026.         d = dialogs.ThreeChoiceDialog(title, description, dialogs.BUTTON_REMOVE_ENTRY, dialogs.BUTTON_DELETE_FILES, dialogs.BUTTON_CANCEL)
  2027.     else:
  2028.         d = dialogs.ChoiceDialog(title, description, dialogs.BUTTON_OK, dialogs.BUTTON_CANCEL)
  2029.     
  2030.     def callback(dialog):
  2031.         if dialog.choice == dialogs.BUTTON_DELETE_FILES:
  2032.             for item in items:
  2033.                 if item.idExists() and isinstance(item, FileItem):
  2034.                     item.deleteFiles()
  2035.                     continue
  2036.             
  2037.         
  2038.         if dialog.choice in (dialogs.BUTTON_OK, dialogs.BUTTON_REMOVE_ENTRY, dialogs.BUTTON_DELETE_FILES):
  2039.             for item in items:
  2040.                 if item.idExists():
  2041.                     item.executeExpire()
  2042.                     continue
  2043.             
  2044.         
  2045.  
  2046.     d.run(callback)
  2047.  
  2048.  
  2049. def getFirstVideoEnclosure(entry):
  2050.     '''Find the first video enclosure in a feedparser entry.  Returns the
  2051.     enclosure, or None if no video enclosure is found.
  2052.     '''
  2053.     
  2054.     try:
  2055.         enclosures = entry.enclosures
  2056.     except (KeyError, AttributeError):
  2057.         return None
  2058.  
  2059.     for enclosure in enclosures:
  2060.         if filetypes.isVideoEnclosure(enclosure):
  2061.             return enclosure
  2062.             continue
  2063.     
  2064.     return None
  2065.  
  2066.  
  2067. def formatRateForDetails(bytes):
  2068.     '''Format a download/upload rate for the more-details view.'''
  2069.     sizeFmt = util.formatSizeForUser(bytes, zeroString = u'-')
  2070.     if bytes > 0:
  2071.         return sizeFmt + u'/s'
  2072.     else:
  2073.         return sizeFmt
  2074.  
  2075. formatRateForDetails = returnsUnicode(formatRateForDetails)
  2076.  
  2077. def formatSizeForDetails(bytes):
  2078.     '''Format a disk size for the more-details view.'''
  2079.     return util.formatSizeForUser(bytes, zeroString = u'-')
  2080.  
  2081. formatSizeForDetails = returnsUnicode(formatSizeForDetails)
  2082.